环境变量


摘要: "环境变量是 Linux 的一个核心概念,很多软件的启动和配置都和它有关"


前言

之前配置过很多服务端应用,对于环境变量的使用是家常便饭,但是有些细枝末节的问题一直没有搞清楚,本文就和环境变量有关的命令来详细梳理下各个容易混淆的概念。


Shell变量

在展开环境变量的内容前,得先说说 Shell 的变量(Variable)。和其他的编程语言一样,变量的作用就是提供一个带名字的容器存储一个可能会改变的值。

这里不会过于详细的讲解 Shell 编程中如何使用变量,只是简单的讲下如何设置变量和查看变量值:

# 定义单个变量
name=djhx

# 定义多个变量
name=djhx age=18

# 打印变量值
echo $name

如果只是在 shell 中定义一个变量,那这个变量只在当前 shell 进程中有效,这一点也就是和环境变量的本质区别。

shell 变量仅存在于当前终端,如果推出了终端或者打开了新的 shell,这个变量就没有了,shell 的子进程也无法访问 shell 变量。


环境变量

shell 变量是用在 shell 编程中的,和别的编程语言的变量差不多,那么环境变量就是另一个概念了,环境变量是为了定义/配置环境,这个环境可以是一个 shell 下的子 shell 进程,可以是一个 Java/Python/Go 后台程序,也可以是 PostgreSQL/MySQL 数据库运行时环境。

换句话来说,运行在电脑上的大部分程序在下载下来都是一致的,但是开发者们为这些软件设置很多开关和配置项,用这些可配置的内容来改变同一个软件在不同环境下的行为,以满足不同场景下的需求。这听起来非常像配置文件,比如各种应用的 conf/ini/yaml/properties/toml 文件,环境变量和配置文件从本质上来说都是为了提供一组配置来更改软件的行为,但是在生产实践中,二者有着很大的功能区分,这种功能区分也是我认为行业内软件开发公司(至少我经历过的)做的非常不足的地方。

先看看配置文件,它们大多是以某种特定格式(yaml、json、toml、ini)编写了很多不同的配置项存储在系统的某个目录下(/etc/your-app/config)。 无论是 toml 还是 json,这种预定义的格式可以支持极其复杂和庞大的配置项,落盘到了文件系统,也意味着其中大部分的配置项是很少会被修改的,一旦修改了配置文件的某个配置项通常需要重启应用或者发送信号通知应用重载配置文件。

几乎所有应用,无论是桌面端还是服务器端都有配置文件,但配置文件的弊端在服务器端尤为明显,那就是安全性和动态性。

开发一个稍稍正式一点,中等规模以上的服务端应用,一套代码难免会需要适配三个环境,开发、测试和生产。不同程序环境下的监听端口,线程池大小,工作进程数量,数据库连接配置,第三方服务密钥和接口,日志级别都不太一样,所以在开发过程中,就至少会有三套配置文件,在开发环境下,使用 dev.conf,在正式环境下使用 prod.conf。

如果是三个配置文件还好管理,但如果服务再复杂些,甚至不同的开发者有着自己的配置文件,管理配置文件本身就非常容易出现问题,那就是密钥的泄露。

接手过的一部分应用是将数据库连接地址和密钥写死在代码里的,并且代码提交到了 git 服务器中,这种情况下,如果密钥泄露了那真是一点都不让人意外。

还有部分应用是做了配置文件的分离和继承,把关于密钥的配置单独放在一个文件中,并且在 .gitignore 中忽略了密钥文件的提交,但谁也不能保证项目目录下的某个包含密钥的文件绝对不会被提交上去(也许改了文件名?)。

上面所说的,这是配置文件的第一个问题:非常容易泄露密钥,因为把密钥写在便利贴上,并且贴在电脑上的方便性实在是过于诱人。

配置文件的第二个问题就是动态性,不过这倒不是缺点,一般会将很少人为修改的配置项写在配置文件,持久化在磁盘中,然而对于经常动态修改的配置项,就需要频繁更新配置文件。

针对以上的两个问题,环境变量是一个很好的解决方法。

环境变量的存储位置是在操作系统进程的内存空间中,它与进程绑定,进程退出,环境变量就消失了,与配置文件各种花里胡哨的配置格式不同,环境变量就是键值对,格式很单一。

环境变量是很动态的,它一般在某个程序进程运行前设置,并且不会持久化到磁盘中,因此安全性相对较高。

查看

想要查看某一个或者所有环境变量,可以使用命令 printenv :

# 查看所有环境变量
printenv

# 查看某个环境变量
printenv PATH
# 或者用 echo
echo $PATH

创建

创建一个环境变量很简单,就是比创建一个普通的 shell 变量前面多了一个 export:

# 创建一个环境变量 APP_NAME
export APP_NAME="A Simple APP"

# 查看创建的环境变量
printenv APP_NAME

环境变量的命名,建议是全字母大写,下划线分隔,这是通俗的约定,不是强制性要求。

删除

使用命令 unset 就可以把创建的环境变量删除:

# 删除一个环境变量
unset APP_NAME

作用域

对于变量而言,一个很重要的概念就是作用域,shell 的变量和环境变量也有作用域,不过相比于其他语言来说更简单一些。

编写以下 sh 脚本:

# echo_my_name.sh
echo $MY_NAME

首先是普通变量,如果单纯只是在 shell 中对一个变量赋值,那么只有当前 shell 可以访问到改变量,其他进程或者子进程是无法访问到这个变量的。

MY_NAME=djhx
# 输出 djhx,说明当前 shell 可以访问改变量
echo $MY_NAME

# 运行 shell 脚本,没有输出,子shell进程是无法访问到普通变量的
sh ./echo_my_name.sh

普通变量不会传递给子进程,只能在当前 shell 进程中访问。

如果想要子进程可以访问,那么需要把变量改成环境变量,之前没有提到的一点是,如果你已经定义了一个普通变量(比如上面的 MY_NAME),那么可以通过 export 把改变量变成环境变量:

export MY_NAME
# 把 MY_NAME 从普通变量变成环境变量后,子shell进程就可以访问了
sh ./echo_my_name.sh

不光是 shell 的子 shell 进程,其他程序(Python/Go/Java/MySQL/PostgreSQL等等)作为当前 shell 进程的子进程,也可以通过这样的方式来传递环境变量。


参考

  1. https://www.cherryservers.com/blog/how-to-set-list-and-manage-linux-environment-variables
  2. https://wiki.archlinux.org/title/Environment_variables
  3. https://www.runoob.com/linux/linux-shell-variable.html
  4. https://www.digitalocean.com/community/tutorials/how-to-view-and-update-the-linux-path-environment-variable
  5. https://12factor.net/config